我们都是黑客
讲一讲数据安全,如何有效预防脱库
1.数据的访问控制
我们先来看看哪些是经常访问数据库的用户?
软件程序(应用程序、数据库中间件)
人员:运维、开发、测试、产品、等
那接下来我们就来看看从这几点如何来控制数据库的访问。
1.1.软件程序层面
java、go、python
或其他,已经有很丰富的orm
和datasource
框架或工具,我下面罗列一些java
中常用jdbc
连接池和orm
框架以及数据库中间件名称 | 说明 | 是否有加解密策略 |
我相信很多程序的数据库连接与密码都是通过配置文件来保存的,假如应用服务器被黑客利用软件漏洞拿下,我相信通过部署的软件可以翻出数据库连接的配置,那么针对这一点我们如何有效的避免呢?
1.1.1.数据库连接密码加密
failover
的时候应用程序可以不需要重启,只用重新创建连接即可。因此这层代理可以有效的防止数据库真实部署的机器被暴漏出去,起到了一定的安全作用。jdbc
连接的时候往往是有密码访问的,我相信很多数据库的密码是明文的存储在配置文件中,虽然现在都用配置中心(configcenter
)来统一管理应用的配置,如果使用明文来保存密码始终是无法规避泄漏的风险,因为应用程序始终要进行连接,在连接的时候要读取配置,不管配置是从云端同步下来还是从本地读取,只要是明文存储密码的就会存在安全问题。druid
举个例子,具体的看看如何使用,也可以查看druid
官方 示例(https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter)ConfigFilter
为数据库密码提供加密功能<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
.................
<!-- 如果fitlers走的配置中心,请去配置中心修改 -->
<property name="filters" value="${filters}" />
<!-- 如果没有配置中心请直接修改 -->
<property name="filters" value="stat,config" />
<!-- 以上两种filters配置2选一 -->
<property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${publickey}" />
.................
</bean>
filters=stat
改为
filters=stat,config
jdbc.xxxx.password=123456
改为
jdbc.xxxx.password=加密后的值
增加
publickey=公钥
jdbc.password=p9i+fChqlaYnfhI+NoJqmrGwTyWwlFZ1W7Vi7i2MGZ8agFkGxGr/kWU//yDvPyXZ6YwJwnMKQ4zXpTZnfxWaRjfqWIRG+JzxSdSYEMp/bRCiIvzF6y8FdVCqN/0m0eQeZFvMCdIf4wqhKF0QRCEOTysZ3oGg7t5o35CIMpV1A5Y=
jdbc
连接池也都有类似的功能,但是不排除有一些没有这个功能的就需要我们自己动手开发来增强这部分功能。dbcp
扩展这个功能。import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import java.util.logging.Logger;
import org.apache.commons.dbcp.BasicDataSource;
import org.slf4j.LoggerFactory;
public class SecurityBasicDataSource extends BasicDataSource {
private final org.slf4j.Logger logger = LoggerFactory.getLogger(SecurityBasicDataSource.class);
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new SQLFeatureNotSupportedException();
}
@Override
public void setPassword(String password) {
try {
//这里可以从任意地方读取数据库配置
Properties p = ConfigLoaderUtils.loadConfig("jdbc.properties");
String publickey = p.getProperty("publickey");
//ConfigTools是实现私钥、公钥对加解密实现
password = ConfigTools.decrypt(publickey, password);
super.setPassword(password);
} catch(Exception e) {
logger.error("解密password出错", e);
}
}
}
首先我们继承
dbcp
数据源org.apache.commons.dbcp.BasicDataSource
重写
setPassword
设置密码的时候通过公钥和密文进行解密
dbcp
扩展了数据库连接加解密的功能,是不是很简单。1.1.2.敏感数据加解密
AES
或DES
,为什么不使用非对称的公开密钥加密 ?DES
作为示例,当然可以替换成任意的加解密算法。java
的annotation
可以帮助我们实现打标import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Cipher {
}
@Cipher
注释,说明这几个字段我们在保存、修改的时候需要加密,在查询的时候需要解密。CRUD
的地方进行加解密调用这样会很傻很天真,作为被广泛使用的orm
框架之一的mybatis
这里我使用它作为示例讲解实现思路。mybatis
提供拦截器机制,可以对执行的CRUD
进行拦截处理操作,pagehelper 是一个分页的mybatis
插件,就是利用拦截的机制来扩展分页功能。insert
、update
操作进行加密,对select
操作进行解密,在mybatis
的底层保存和修改都是update
方法,查询都是query
方法,刚好我们就对这两个方法进行拦截处理。import java.lang.reflect.Field;
import java.util.List;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Intercepts({
@Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }) })
public class CipherHelper implements Interceptor {
private final Logger logger = LoggerFactory.getLogger(CipherHelper.class);
/**
* 加密密钥</br> 为null,使用默认密钥进行加解密</br>
*/
private String secureKey = null;
/**
* 是否允许宽容处理</br> 宽容处理的话,使用原值,反之throw {@link CipherException}</br>
*/
private boolean lenient = false;
@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
if (methodName.equals("update") || methodName.equals("query")) {
Object parameter = invocation.getArgs()[1];
if (parameter instanceof List) {
List<?> list = (List<?>) parameter;
for (Object obj : list) {
encrypt(obj);
}
} else if(parameter instanceof StrictMap) {
StrictMap<?> strictMap = (StrictMap<?>) parameter;
if (strictMap.containsKey("list")) {
List<?> list = (List<?>) strictMap.get("list");
for (Object obj : list) {
encrypt(obj);
}
} else if (strictMap.containsKey("array")) {
Object[] objects = (Object[]) strictMap.get("array");
for (Object obj : objects) {
encrypt(obj);
}
}
} else {
encrypt(parameter);
}
}
Object returnValue = invocation.proceed();
if (methodName.equals("query")) {
if (returnValue instanceof List) {
List<?> list = (List<?>) returnValue;
for (Object obj : list) {
decrypt(obj);
}
} else {
decrypt(returnValue);
}
}
return returnValue;
}
/**
* 加密处理
*
* @param parameter
* @throws IllegalAccessException
*/
private void encrypt(Object parameter) throws IllegalAccessException {
if (parameter == null) return;
Class<?> clazz = parameter.getClass();
if (!clazz.getSimpleName().endsWith("Entity")) {
return;
}
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
if (!fields[i].isAnnotationPresent(Cipher.class)) {
continue;
}
if (!fields[i].getType().equals(String.class)) {
logger.debug("加密字段只支持String类型,当前类型非String,跳过!");
continue;
}
fields[i].setAccessible(true);
String v = (String) fields[i].get(parameter);
if (StringUtils.isBlank(v)) {
logger.debug("加密字段值为null,跳过!");
continue;
}
try {
String crypt = DESTools.encrypt(secureKey, v);
fields[i].set(parameter, crypt);
logger.debug("加密处理字段,{}", fields[i].getName());
} catch (Exception e) {
if (lenient) {
logger.warn("加密处理失败,宽容处理使用原值");
} else {
throw new CipherException("加密处理失败,不允许宽容处理["+v+"]", e);
}
}
}
}
}
/**
* 解密处理
*
* @param obj
* @throws IllegalAccessException
* @throws Exception
*/
private void decrypt(Object obj) throws IllegalAccessException, Exception {
if (obj == null) return;
Class<?> clazz = obj.getClass();
if (!clazz.getSimpleName().endsWith("Entity")) {
return;
}
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
if (!fields[i].isAnnotationPresent(Cipher.class)) {
continue;
}
if (!fields[i].getType().equals(String.class)) {
logger.debug("解密字段只支持String类型,当前类型非String,跳过!");
continue;
}
fields[i].setAccessible(true);
String v = (String) fields[i].get(obj);
if (StringUtils.isBlank(v)) {
logger.debug("解密字段值为null,跳过!");
continue;
}
try {
String crypt = DESTools.decrypt(secureKey, v);
fields[i].set(obj, crypt);
logger.info("解密处理字段,{}", fields[i].getName());
} catch (Exception e) {
if (lenient) {
logger.warn("解密处理失败,宽容处理使用原值");
} else {
throw new CipherException("解密处理失败,不允许宽容处理["+v+"]", e);
}
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
if (properties != null && StringUtils.isNotBlank(properties.getProperty("secureKey"))) {
this.secureKey = properties.getProperty("secureKey");
}
if (properties != null && StringUtils.isNoneBlank(properties.getProperty("lenient"))) {
this.lenient = Boolean.parseBoolean(properties.getProperty("lenient"));
}
}
}
mybatis
的插件扩展机制在执行过程进行拦截处理,plugin
方法是插件的装载方法,setProperties
方法设置关键属性,比如说密钥串。encrypt
:是加密方法,这里加密方法需要注意的是,mybatis
参数支持Pojo
、Map
、StrictMap
、List
、Array
,我们使用注解@Cipher
是用在类上的所以只对Pojo
生效,如果是Map
它天生的key,value
格式无法支持打标,我们这里对Map
类型进行跳过不处理,如果非要处理Map
也是有办法的,需要固定加解密的key
值,对特定的key
进行识别并加解密替换value
,加密方法通过查找有注解@Cipher
的字段进行加密并且回填值。decrypt
:是解密方法,主要用在查询时的解密,这里需要注意的是查询有可能返回特定的Pojo
也可能返回List
,所以这里解密的时候需要根据类型来分别处理,如果是List
需要进行很层次查找,如果是Pojo
那就查找使用注解@Cipher
的字段进行解密并且回填值。intercept
:是拦截方法,在update
、query
前后进行拦截处理,在这个方法里需要进行如下步骤:识别当前执行的
method
是update
还是query
如果是
update
那就进行加密如果是
query
那就进行解密识别参数类型是
List
、StrictMap
、Pojo
如果
StrictMap
里是list
那就循环调用encrypt
方法如果
StrictMap
里是array
那就循环调用encrypt
方法如果
List
里是Pojo
那就循环调用encrypt
方法如果
List
里是Map
跳过处理,或者使用上面我们说的识别某些固定key
进行加密处理如果是
List
需要再深层次看一下List
里是什么类型,这里建议使用递归方式如果是
StrictMap
需要再深层次看一下StrictMap
里是什么类型,这里建议使用递归方式如果是
Pojo
那就调用encrypt
方法执行sql处理获取返回值
获取返回值并且执行的方法是
query
时,进行解密处理如果是
List
深层次查找内部类型,这里建议使用递归方式如果是
Pojo
那就调用decrypt
方法如果
List
里是Pojo
那就循环调用decrypt
方法如果
List
里是Map
跳过处理,或者使用上面我们说的识别某些固定key
进行解密处理识别返回的类型是
List
还是Pojo
1.2.人员层面
前面我们说了绝大多数的数据泄密都不是技术问题而是人员管理问题,我们要对人员进行有效的管理与控制。
1.2.1.开发或测试人员
这类人一般对数据是有CRUD
的诉求,针对这类人员的控制有如下几个方面
开发人员只能连接测试环境数据库,不允许连接生产数据库,即使连接vpn也不行。
开发人员申请数据库需要走运维工单流程,运维提供数据库连接密码时应直接提供密文,或者运维直接给配置到配置中心。
配置文件或配置中心禁止存储明文密码,需要对jdbc等其他敏感密码进行脱敏处理。
生产服务器需要通过跳板机访问,禁止开发使用
root
直接操作,如果看应用日志可以走日志平台,实在没有日志平台可以给跳板机开通app
用户只给查看固定目录日志的权限,如果要发布走devops
平台,如果没有可以提供给运维进行发布。查询生产数据走
dms
平台,对敏感信息进行脱敏或隐藏,对上线的sql和日常的查询日志做到dms
可管控。微信搜索公众号:Linux技术迷,回复:linux 领取资料 。
提交到开放环境时需要注意以下几点
提交到开放的仓库(
github
、gitlab
、gitee
等),需要对代码进行审核,避免有hardcode
的公司服务器密码、ip、端口、密钥等。提交到开放的论坛(
csdn
、oschina
、知乎
、公众号
、社区分享
等),需要对文章进行审核,避免有不允许公开的技术细节或敏感信息。
1.2.2.运维或DBA人员
这类人一般操作权限都很高,出问题概率最高的人员,有很多删库跑路或误操作rm -rf
的例子哈哈哈!所以这类人更要重点管控。
需要搭建和处理运维工单平台,用于开发提出的运维资源申请,尤其是数据库密码,直接提供加密后的密文和公钥。
需要搭建和处理数据库管理工具
dms
,用于开发日常生产数据查询和发版时SQL
升级。需要提供跳板机和给跳板机提供不同等级的用户,提供给特别需要的人访问生产环境机器。
需要提供
devops
平台或者自动化发版工具,避免手动操作失误带来问题,对开发提供升级发布的流水线。对服务器密码需要进行加密存储,可以借助密码管理工具。
运维最好也不要使用
root
用户操作服务器,使用特定权限的用户操作。dba
最好也不要使用root
用户操作数据库,使用特定权限的用户操作。制定责任人机制,对应的责任项必须到具体人,具体可以参考 责任分配矩阵RAM 。
关键重要的操作需要至少两个人在场,具体可以参考 责任分配矩阵RAM 。
1.2.3.产品或业务人员
这类人一般对数据有查询和分析的诉求,有分析诉求就需要导出数据,所以分析诉求统一走公司BI
工具,也有少部分有修改的诉求。
查询分析数据,统一接入
BI
工具,并且BI
工具需要有功能和数据权限,并对敏感数据导出加以控制,导出走审批并脱敏。提交数据变更,统一接入
dms
平台。产品的分析文件(word、excel、ppt)应该进行加密,这种一般依赖公司引入文档安全的解决方案,要花钱的,如果不想花钱那就没啥好办法。
2.总结
如喜欢本文,请点击右上角,把文章分享到朋友圈
作者:凝雨
来源:https://ningyu1.github.io/20201229/datasource-security.html
版权申明:内容来源网络,仅供分享学习,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!